package com.content.scaffold.common.client.proxy;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;

import com.content.scaffold.common.exception.BaseException;
import com.content.scaffold.common.utils.RedisUtils;
import com.content.scaffold.common.web.ResponseCodeEnum;
import com.google.common.collect.ImmutableList;
import com.notarycloud.client.NotaryCloudClient;
import com.notarycloud.client.NotaryCloudResponse;
import com.notarycloud.client.NotaryCloudSimpleRequest;
import com.notarycloud.client.NotaryCloudSimpleResponse;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.DigestUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * @author xiaojl
 */
@Slf4j
@Data
public class NotaryCloudRequestAdvise implements MethodInterceptor {

    private RedisUtils redisUtils;

    private static final String REDIS_CLOUD_CLIENT_LOCK = "fund:base:lock:%s:%s";

    private static final String REDIS_CLOUD_CLIENT_DATA = "fund:base:data:%s:%s";

    private static final String REDIS_CLOUD_CLIENT_BACKUP = "fund:base:backup:%s:%s";

    private static final ImmutableList<Object> IGNORE_PATH = ImmutableList.builder()
            .build();

    private static final ImmutableList<Object> CACHE_PATHS = ImmutableList.builder()
            .add("/api/template").add("/api/open").build();


    private static final ImmutableList<Object> NO_CACHE_PATHS = ImmutableList.builder()
            .add("/api/open/menu/build")
            .build();

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Object obj = methodInvocation.getThis();
        Method getDataMethod = obj.getClass().getSuperclass().getDeclaredMethod("getData");
        getDataMethod.setAccessible(true);
        Method getPathMethod = obj.getClass().getSuperclass().getDeclaredMethod("getPath");
        Object data = getDataMethod.invoke(obj);
        getPathMethod.setAccessible(true);
        Object path = getPathMethod.invoke(obj);
        if ( null != redisUtils && obj instanceof NotaryCloudSimpleRequest ) {
            String pathMd5 = DigestUtils.md5DigestAsHex(path.toString().getBytes());
            String paramsStr = "";
            if (data instanceof JSONObject) {
                paramsStr = data.toString();
            }else{
                paramsStr = JSONUtil.toJsonStr(data);
            }
            String paramMd5 = DigestUtils.md5DigestAsHex(paramsStr.getBytes());
           JSONObject backupInfo = new JSONObject();
            backupInfo.put("path", path);
            backupInfo.put("params", paramsStr);
            backupInfo.put("type", methodInvocation.getMethod().getName());
            //保存请求地址和参数，供定时任务更新
            redisUtils.set(redisBackupKey(pathMd5, paramMd5), backupInfo.toString(), (long) 23 * 60L * 60);

            return getObject(methodInvocation, obj, pathMd5, paramMd5);
        }
        return null;
    }

    private Object getObject(MethodInvocation methodInvocation, Object obj, String pathMd5, String paramMd5) throws Throwable {
        Object retVal;
        if (redisUtils.hasKey(redisDataKey(pathMd5, paramMd5))) {
            ResponseData responseData = JSONUtil.toBean(redisUtils.get(redisDataKey(pathMd5, paramMd5)).toString(), ResponseData.class);
            NotaryCloudSimpleResponse response = new NotaryCloudSimpleResponse();
            response.setCode(responseData.getCode());
            response.setData(responseData.getData());
            response.setMessage(responseData.getMessage());
            response.setRequestId(responseData.getRequestId());
            response.setNeedDecrypt(responseData.isNeedDecrypt());
            Field clientField = obj.getClass().getSuperclass().getDeclaredField("client");
            clientField.setAccessible(true);
            Method setClientMethod = response.getClass().getDeclaredMethod("setClient", NotaryCloudClient.class);
            setClientMethod.setAccessible(true);
            setClientMethod.invoke(response, clientField.get(obj));
            retVal = response;
            if (!redisUtils.hasKey(redisLockKey(pathMd5, paramMd5))) {
                asyncProceed(methodInvocation, pathMd5, paramMd5);
            }
        } else {
            retVal = methodInvocation.proceed();
            saveReturnInfoToRedis(pathMd5, paramMd5, (NotaryCloudResponse) retVal);
        }
        return retVal;
    }

    private boolean isCache(String path) {
        if (StringUtils.isEmpty(path)) {
            return false;
        }
        Iterator iter = NO_CACHE_PATHS.stream().iterator();
        while (iter.hasNext()) {
            if (path.startsWith((String) iter.next())) {
                return false;
            }
        }
        Iterator it = CACHE_PATHS.stream().iterator();
        while (it.hasNext()) {
            if (path.startsWith((String) it.next())) {
                return true;
            }
        }
        return false;
    }

    private String redisLockKey(String path, String data) {
        return String.format(REDIS_CLOUD_CLIENT_LOCK, path, data);
    }

    private String redisDataKey(String path, String data) {
        return String.format(REDIS_CLOUD_CLIENT_DATA, path, data);
    }

    private String redisBackupKey(String path, String data) {
        return String.format(REDIS_CLOUD_CLIENT_BACKUP, path, data);
    }

    private Object asyncProceed(MethodInvocation methodInvocation, String pathMd5, String paramMd5) {
        try {
            CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
                try {
                    Object retVal = methodInvocation.proceed();
                    saveReturnInfoToRedis(pathMd5, paramMd5, (NotaryCloudResponse) retVal);
                    return retVal;
                } catch (Throwable throwable) {
                    log.error(throwable.getMessage(), throwable);
                }
                return null;
            });
            return future.get();
        } catch (InterruptedException | ExecutionException e) {
            log.error(e.getMessage());
            Thread.currentThread().interrupt();
            throw new BaseException(ResponseCodeEnum.ERROR, e.getMessage());
        }
    }

    private void saveReturnInfoToRedis(String pathMd5, String paramMd5, NotaryCloudResponse retVal) {
        if (retVal.getCode() != 0) {
            return;
        }
        redisUtils.set(redisLockKey(pathMd5, paramMd5), true, 60);
        ResponseData responseData = new ResponseData();
        responseData.setCode(retVal.getCode());
        responseData.setData(retVal.getData());
        responseData.setMessage(retVal.getMessage());
        responseData.setRequestId(retVal.getRequestId());
        responseData.setNeedDecrypt(retVal.isNeedDecrypt());
        redisUtils.set(redisDataKey(pathMd5, paramMd5), JSONUtil.toJsonStr(responseData), (long) 60 * 60 * 24);
    }

    @Data
    private static class ResponseData {
        int code;
        String message;
        String data;
        String requestId;
        boolean isNeedDecrypt;
    }
}
